{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Project 1: Setting up Transaction Codes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Our code so far:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import itertools\n",
    "import numbers\n",
    "from datetime import timedelta\n",
    "\n",
    "class TimeZone:\n",
    "    def __init__(self, name, offset_hours, offset_minutes):\n",
    "        if name is None or len(str(name).strip()) == 0:\n",
    "            raise ValueError('Timezone name cannot be empty.')\n",
    "            \n",
    "        self._name = str(name).strip()\n",
    "        # technically we should check that offset is a\n",
    "        if not isinstance(offset_hours, numbers.Integral):\n",
    "            raise ValueError('Hour offset must be an integer.')\n",
    "        \n",
    "        if not isinstance(offset_minutes, numbers.Integral):\n",
    "            raise ValueError('Minutes offset must be an integer.')\n",
    "            \n",
    "        if offset_minutes < -59 or offset_minutes > 59:\n",
    "            raise ValueError('Minutes offset must between -59 and 59 (inclusive).')\n",
    "            \n",
    "        # for time delta sign of minutes will be set to sign of hours\n",
    "        offset = timedelta(hours=offset_hours, minutes=offset_minutes)\n",
    "\n",
    "        # offsets are technically bounded between -12:00 and 14:00\n",
    "        # see: https://en.wikipedia.org/wiki/List_of_UTC_time_offsets\n",
    "        if offset < timedelta(hours=-12, minutes=0) or offset > timedelta(hours=14, minutes=0):\n",
    "            raise ValueError('Offset must be between -12:00 and +14:00.')\n",
    "            \n",
    "        self._offset_hours = offset_hours\n",
    "        self._offset_minutes = offset_minutes\n",
    "        self._offset = offset\n",
    "        \n",
    "    @property\n",
    "    def offset(self):\n",
    "        return self._offset\n",
    "    \n",
    "    @property\n",
    "    def name(self):\n",
    "        return self._name\n",
    "    \n",
    "    def __eq__(self, other):\n",
    "        return (isinstance(other, TimeZone) and \n",
    "                self.name == other.name and \n",
    "                self._offset_hours == other._offset_hours and\n",
    "                self._offset_minutes == other._offset_minutes)\n",
    "    def __repr__(self):\n",
    "        return (f\"TimeZone(name='{self.name}', \"\n",
    "                f\"offset_hours={self._offset_hours}, \"\n",
    "                f\"offset_minutes={self._offset_minutes})\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Account:\n",
    "    transaction_counter = itertools.count(100)\n",
    "    _interest_rate = 0.5  # percentage\n",
    "    \n",
    "    def __init__(self, account_number, first_name, last_name, \n",
    "                 timezone=None, initial_balance=0):\n",
    "        # in practice we probably would want to add checks to \n",
    "        # make sure these values are valid / non-empty\n",
    "        self._account_number = account_number\n",
    "        self.first_name = first_name\n",
    "        self.last_name = last_name\n",
    "        \n",
    "        if timezone is None:\n",
    "            timezone = TimeZone('UTC', 0, 0)\n",
    "        self.timezone = timezone\n",
    "        \n",
    "        self._balance = float(initial_balance)  # force use of floats here, but maybe Decimal would be better\n",
    "        \n",
    "    @property\n",
    "    def account_number(self):\n",
    "        return self._account_number\n",
    "    \n",
    "    @property \n",
    "    def first_name(self):\n",
    "        return self._first_name\n",
    "    \n",
    "    @first_name.setter\n",
    "    def first_name(self, value):\n",
    "        self.validate_and_set_name('_first_name', value, 'First Name')\n",
    "        \n",
    "    @property\n",
    "    def last_name(self):\n",
    "        return self._last_name\n",
    "    \n",
    "    @last_name.setter\n",
    "    def last_name(self, value):\n",
    "        self.validate_and_set_name('_last_name', value, 'Last Name')\n",
    "        \n",
    "    # also going to create a full_name computed property, for ease of use\n",
    "    @property\n",
    "    def full_name(self):\n",
    "        return f'{self.first_name} {self.last_name}'\n",
    "        \n",
    "    @property\n",
    "    def timezone(self):\n",
    "        return self._timezone\n",
    "    \n",
    "    @property\n",
    "    def balance(self):\n",
    "        return self._balance\n",
    "    \n",
    "    @timezone.setter\n",
    "    def timezone(self, value):\n",
    "        if not isinstance(value, TimeZone):\n",
    "            raise ValueError('Time zone must be a valid TimeZone object.')\n",
    "        self._timezone = value\n",
    "          \n",
    "    @classmethod\n",
    "    def get_interest_rate(cls):\n",
    "        return cls._interest_rate\n",
    "    \n",
    "    @classmethod\n",
    "    def set_interest_rate(cls, value):\n",
    "        if not isinstance(value, numbers.Real):\n",
    "            raise ValueError('Interest rate must be a real number')\n",
    "        if value < 0:\n",
    "            raise ValueError('Interest rate cannot be negative.')\n",
    "        cls._interest_rate = value\n",
    "        \n",
    "    def validate_and_set_name(self, property_name, value, field_title):\n",
    "        if value is None or len(str(value).strip()) == 0:\n",
    "            raise ValueError(f'{field_title} cannot be empty.')\n",
    "        setattr(self, property_name, value)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Although we _could_ use hardcoded values for the `D`, `W`, `I`, and `X` transaction codes, I prefer to store them in a dictionary and lookup the code whenever I need to. That way, if we ever need to change those codes for some reason, we don't have to hunt them down in the code itself.\n",
    "\n",
    "So for that, I'm going to use a \"private\" class attribute (dictionary), with the assumption that the keys will **not** change, but the associated values (codes) **can**. We could actually go on step further and define \"constants\" for the keys as well, but I don't think that's really necessary.\n",
    "\n",
    "A better approach would be to use an **enumeration** type - but we're not there yet!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Account:\n",
    "    transaction_counter = itertools.count(100)\n",
    "    _interest_rate = 0.5  # percentage\n",
    "    \n",
    "    _transaction_codes = {\n",
    "        'deposit': 'D',\n",
    "        'withdraw': 'W',\n",
    "        'interest': 'I',\n",
    "        'rejected': 'X'\n",
    "    }\n",
    "    \n",
    "    def __init__(self, account_number, first_name, last_name, timezone=None, initial_balance=0):\n",
    "        # in practice we probably would want to add checks to make sure these values are valid / non-empty\n",
    "        self._account_number = account_number\n",
    "        self.first_name = first_name\n",
    "        self.last_name = last_name\n",
    "        \n",
    "        if timezone is None:\n",
    "            timezone = TimeZone('UTC', 0, 0)\n",
    "        self.timezone = timezone\n",
    "        \n",
    "        self._balance = float(initial_balance)  # force use of floats here, but maybe Decimal would be better\n",
    "        \n",
    "    @property\n",
    "    def account_number(self):\n",
    "        return self._account_number\n",
    "    \n",
    "    @property \n",
    "    def first_name(self):\n",
    "        return self._first_name\n",
    "    \n",
    "    @first_name.setter\n",
    "    def first_name(self, value):\n",
    "        self.validate_and_set_name('_first_name', value, 'First Name')\n",
    "        \n",
    "    @property\n",
    "    def last_name(self):\n",
    "        return self._last_name\n",
    "    \n",
    "    @last_name.setter\n",
    "    def last_name(self, value):\n",
    "        self.validate_and_set_name('_last_name', value, 'Last Name')\n",
    "        \n",
    "    # also going to create a full_name computed property, for ease of use\n",
    "    @property\n",
    "    def full_name(self):\n",
    "        return f'{self.first_name} {self.last_name}'\n",
    "        \n",
    "    @property\n",
    "    def timezone(self):\n",
    "        return self._timezone\n",
    "    \n",
    "    @property\n",
    "    def balance(self):\n",
    "        return self._balance\n",
    "    \n",
    "    @timezone.setter\n",
    "    def timezone(self, value):\n",
    "        if not isinstance(value, TimeZone):\n",
    "            raise ValueError('Time zone must be a valid TimeZone object.')\n",
    "        self._timezone = value\n",
    "          \n",
    "    @classmethod\n",
    "    def get_interest_rate(cls):\n",
    "        return cls._interest_rate\n",
    "    \n",
    "    @classmethod\n",
    "    def set_interest_rate(cls, value):\n",
    "        if not isinstance(value, numbers.Real):\n",
    "            raise ValueError('Interest rate must be a real number')\n",
    "        if value < 0:\n",
    "            raise ValueError('Interest rate cannot be negative.')\n",
    "        cls._interest_rate = value\n",
    "        \n",
    "    def validate_and_set_name(self, property_name, value, field_title):\n",
    "        if value is None or len(str(value).strip()) == 0:\n",
    "            raise ValueError(f'{field_title} cannot be empty.')\n",
    "        setattr(self, property_name, value)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
